﻿using System;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Fibre.Threading.Tests
{
    [TestClass]
    public class ViewModelBaseTests 
    {
        [TestMethod]
        public void PerformWorkRunsOnBackgroundThread()
        {
            var vm = new AsyncWork();

            var testWaitHandle = new ManualResetEvent(false);
            var resultWaitHandle = new ManualResetEvent(false);
            var handleResult = false;

            //Need two wait handles, one to make sure testWaitHandle is set
            //If Start is a blocking call then it will deadlock until timeout (5secs)
            //And the test will fail
            vm.AddWork(() =>
                           {
                               handleResult = testWaitHandle.WaitOne(5000);
                               resultWaitHandle.Set();
                           });

            vm.PerformWork();
            testWaitHandle.Set();
            //Blocks until result is available
            resultWaitHandle.WaitOne(5000);
            Assert.IsTrue(handleResult);
        }

        [TestMethod]
        public void TwoSynchronousWorkItemsAreCompletedConcurrently()
        {
            var vm = new AsyncWork();

            var workItem1WaitHandle = new ManualResetEvent(false);
            var workItem2WaitHandle = new ManualResetEvent(false);

            //Each work item simply signals the other work items wait handle
            //If work items are not run concurrently then one of the work items
            //Will block until timeout
            vm
                .AddWork(() =>
                                {
                                    workItem2WaitHandle.Set();
                                    workItem1WaitHandle.WaitOne(5000);
                                })
                .AddWork(() =>
                                {
                                    workItem1WaitHandle.Set();
                                    workItem2WaitHandle.WaitOne(5000);
                                })
                .PerformWork();

            //Check both waithandles have been signaled
            Assert.IsTrue(workItem1WaitHandle.WaitOne(10000) && workItem2WaitHandle.WaitOne(10000));
        }

        [TestMethod]
        public void CompletedCallbackIsOnlyCalledAfterBothWorkItemsComplete()
        {
            var workItem1WaitHandle = new ManualResetEvent(false);
            var workItem2WaitHandle = new ManualResetEvent(false);
            var completedWaitHandle = new ManualResetEvent(false);

            var vm = new AsyncWork();


            //Each work item simply signals the other work items wait handle
            //If work items are not run concurrently then one of the work items
            //Will block until timeout
            vm
                .AddWork(() =>
                             {
                                 workItem2WaitHandle.Set();
                                 workItem1WaitHandle.WaitOne(5000);
                             })
                .AddWork(() =>
                             {
                                 workItem1WaitHandle.Set();
                                 workItem2WaitHandle.WaitOne(5000);
                             })
                .WhenAllComplete(() =>
                                     {
                                         //Both waitHandles should have been signalled by now
                                         if (!workItem1WaitHandle.WaitOne(10000) ||
                                             !workItem2WaitHandle.WaitOne(10000))
                                         {
                                             Assert.Fail("At least one workItem has not been signaled");
                                         }
                                         completedWaitHandle.Set();
                                     })
                .PerformWork();

            //Block test until completed event has fired and test has fully run
            Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void TestSynchronousWorkItemReturningData()
        {
            var vm = new AsyncWork();
            var resultWaitHandle = new ManualResetEvent(false);
            vm
                .AddWork(() => "result")
                .WhenComplete(result =>
                                  {
                                      Assert.AreEqual("result", result.Result);
                                      resultWaitHandle.Set();
                                  })
                .PerformWork();

             //Block test until completed event has fired and test has fully run
             Assert.IsTrue(resultWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void ChainedWorkItemGetsRun()
        {
            var completedWaitHandle = new ManualResetEvent(false);

            var vm = new AsyncWork();

            var hasRun = false;
            vm
                .AddWork(() => { })
                .WhenComplete(
                    c => c.AddWork(() =>
                                      {
                                          hasRun = true;
                                      }))
                .WhenAllComplete(() =>
                                     {
                                         Assert.IsTrue(hasRun);
                                         completedWaitHandle.Set();
                                     })
                .PerformWork();


            //Block test until completed event has fired and test has fully run
            Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void CompletedCallbackIsOnlyCalledAfterBothChainedWorkItemsComplete()
        {
            var completedWaitHandle = new ManualResetEvent(false);
            var workItem2Complete = false;

            var vm = new AsyncWork();

            var hasRun = false;
            vm
                .AddWork(() => { })
                .WhenComplete(
                    c => c
                             .AddWork(() =>
                                          {
                                              hasRun = true;
                                          })
                             .WhenComplete(
                                 c2 =>
                                     {
                                         if (hasRun)
                                             workItem2Complete = true;
                                     }))
                .WhenAllComplete(() =>
                                     {
                                         Assert.IsTrue(hasRun);
                                         Assert.IsTrue(workItem2Complete);
                                         completedWaitHandle.Set();
                                     })
                .PerformWork();

            //Block test until completed event has fired and test has fully run
            Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void CompletedCallbackIsOnlyCalledAfterBothChainedWorkItemsCompleteForFuncOfTWork()
        {
            var completedWaitHandle = new ManualResetEvent(false);
            var workItem2Complete = false;

            var vm = new AsyncWork();

            var hasRun = false;
            vm
                .AddWork(() => "stuff")
                .WhenComplete(
                    c => c
                                .AddWork(() =>
                                {
                                    hasRun = true;
                                    return "stuff";
                                })
                                .WhenComplete(
                                            c2 =>
                                            {
                                                if (hasRun)
                                                    workItem2Complete = true;
                                            }))
                .WhenAllComplete(() =>
                {
                    Assert.IsTrue(hasRun);
                    Assert.IsTrue(workItem2Complete);
                    completedWaitHandle.Set();
                })
                .PerformWork();

            
            //Block test until completed event has fired and test has fully run
            Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void ThirdLevelOfChainingGetsCalledCorrectly()
        {
            var completedWaitHandle = new ManualResetEvent(false);
            var workItem2Complete = false;

            var vm = new AsyncWork();

            var hasRun = false;
            var has2Run = false;
            vm
                .AddWork(() => { })
                .WhenComplete(
                    c => c
                             .AddWork(() =>
                                         {
                                             hasRun = true;
                                         })
                                         .WhenComplete(
                                     c2 =>
                                         {
                                             if (hasRun)
                                                 workItem2Complete = true;
                                             c2.AddWork(() =>
                                                           {
                                                               if (workItem2Complete)
                                                                   has2Run = true;
                                                           });
                                         }))
                .WhenAllComplete(() =>
                                     {
                                         Assert.IsTrue(hasRun);
                                         Assert.IsTrue(workItem2Complete);
                                         Assert.IsTrue(has2Run);
                                         completedWaitHandle.Set();
                                     })
                .PerformWork();

             //Block test until completed event has fired and test has fully run
             Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void ThirdLevelOfChainingGetsCalledCorrectlyForFuncOfT()
        {
            var completedWaitHandle = new ManualResetEvent(false);
            var workItem2Complete = false;

            var vm = new AsyncWork();

            var hasRun = false;
            var has2Run = false;
            vm
                .AddWork(() => "Stuff")
                .WhenComplete(
                    c => c
                             .AddWork(() =>
                             {
                                 hasRun = true;
                                 return "Stuff";
                             })
                             .WhenComplete(
                                     c2 =>
                                     {
                                         if (hasRun)
                                             workItem2Complete = true;
                                         c2.AddWork(() =>
                                         {
                                             if (workItem2Complete)
                                                 has2Run = true;
                                         });
                                     }))
                .WhenAllComplete(() =>
                {
                    Assert.IsTrue(hasRun);
                    Assert.IsTrue(workItem2Complete);
                    Assert.IsTrue(has2Run);
                    completedWaitHandle.Set();
                })
                .PerformWork();

            //Block test until completed event has fired and test has fully run
            Assert.IsTrue(completedWaitHandle.WaitOne(20000));
        }

        [TestMethod]
        public void DoesNotRunAnythingIfNoWorkHasBeenAdded()
        {
            var vm = new AsyncWork();

            vm.PerformWork();
            vm.WaitForCompletion();
        }

        [TestMethod]
        public void WaitForCompletionBlocksUntilWorkComplete()
        {
            var waitHandle = new ManualResetEvent(false);
            var vm = new AsyncWork();
            var waitHandleSet = true;

            vm
                .AddWork(() => waitHandle.WaitOne(1000))
                .WhenComplete(c => waitHandleSet = c.Result);

            vm.PerformWork();
            vm.WaitForCompletion();

            waitHandle.Set();

            Assert.IsFalse(waitHandleSet);
        }

        [TestMethod]
        public void AsyncResultTest()
        {
            var unitOfWork = new AsyncWork();
            var actionInvoked = false;

            var action = (Action) (() => { actionInvoked = true; });

            unitOfWork
                .AddWork(action.BeginInvoke, action.EndInvoke)
                .PerformWork();
            unitOfWork.WaitForCompletion();

            Assert.IsTrue(actionInvoked);
        }

        [TestMethod]
        public void ActionRunsWorkBeforeCompletion()
        {
            var vm = new AsyncWork();

            var workWaitHandle = new ManualResetEvent(false);

            var actionRun = false;

            //Each work item simply signals the other work items wait handle
            //If work items are not run concurrently then one of the work items
            //Will block until timeout
            vm
                .AddWork(() =>
                {
                    workWaitHandle.Set();
                })
                .WhenComplete(c =>
                {
                    if (workWaitHandle.WaitOne(5000))
                    {
                        actionRun = true;
                    }
                })
                .PerformWork();
            vm.WaitForCompletion();

            //Check both waithandles have been signaled
            Assert.IsTrue(actionRun);
        }

        [TestMethod]
        public void TestNewUnitOfWorkSyntax()
        {
            var vm = new AsyncWork();

            var action = (Func<string>)(() => "blah");

            vm
                .AddWork<string>(action.BeginInvoke, action.EndInvoke)
                .WhenComplete(c => Assert.AreEqual("blah", c.Result));
        }

        [TestMethod]
        public void CanAddMoreWorkOnceWorkHasCompleted()
        {
            var async = new AsyncWork();
            var workItem1WaitHandle = new ManualResetEvent(false);

            async
                .AddWork(() => "blah")
                .PerformWork()
                .WaitForCompletion();

            async
                .AddWork(() => { })
                .WhenComplete(c => workItem1WaitHandle.Set())
                .PerformWork();

            Assert.IsTrue(workItem1WaitHandle.WaitOne(5000));
        }

        [TestMethod]
        public void AsyncPatternWorkThatThrowsExceptionCallsCompletionCallback()
        {
            var async = new AsyncWork();
            var testAsync = new EndThrowsExceptionAsyncClass();
            var workItem1WaitHandle = new ManualResetEvent(false);

            async
                .AddWork<string>(testAsync.BeginTest, testAsync.EndTest)
                .WhenComplete(c =>
                                  {
                                      workItem1WaitHandle.Set();
                                      Assert.IsNotNull(c.Error);
                                  })
                .PerformWork();

            Assert.IsTrue(workItem1WaitHandle.WaitOne());
        }
    }
}